package com.xyq.judge.service.impl;

import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.date.StopWatch;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.xyq.common.core.constants.Constants;
import com.xyq.common.core.constants.JudgeConstants;
import com.xyq.common.core.enums.CodeRunStatus;
import com.xyq.judge.callback.DockerStartResultCallback;
import com.xyq.judge.callback.StatisticsCallback;
import com.xyq.judge.domain.CompileResult;
import com.xyq.judge.domain.SandBoxExecuteResult;
import com.xyq.judge.service.SandboxService;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.CreateContainerCmd;
import com.github.dockerjava.api.command.CreateContainerResponse;
import com.github.dockerjava.api.command.ExecCreateCmdResponse;
import com.github.dockerjava.api.command.ListImagesCmd;
import com.github.dockerjava.api.command.PullImageCmd;
import com.github.dockerjava.api.command.PullImageResultCallback;
import com.github.dockerjava.api.command.StatsCmd;
import com.github.dockerjava.api.model.Bind;
import com.github.dockerjava.api.model.HostConfig;
import com.github.dockerjava.api.model.Image;
import com.github.dockerjava.api.model.Volume;
import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.core.DockerClientBuilder;
import com.github.dockerjava.netty.NettyDockerCmdExecFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.File;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Service
public class SandboxServiceImpl implements SandboxService {


    // docker连接的地址, 默认值 localhost2375
    @Value("${sandbox.docker.host:tcp://localhost:2375}")
    private String dockerHost;

    // 内存限制
    @Value("${sandbox.limit.memory:100000000}")
    private Long memoryLimit;
    // 交换内存限制
    @Value("${sandbox.limit.memory-swap:100000000}")
    private Long memorySwapLimit;

    // cpu限制 默认 1
    @Value("${sandbox.limit.cpu:1}")
    private Long cpuLimit;

    // 执行代码执行时间 默认: 6秒
    @Value("${sandbox.limit.time}")
    private Long timeLimit;
    // 用户代码文件区分 目录
    private String userCodeDir;
    // 用户代码文件名字
    private String userCodeFileName;
    // docker的连接
    private DockerClient dockerClient;
    // 容器id ,后面用来操作这个容器
    private String containerId;


    @Override
    public SandBoxExecuteResult exeJavaCode(Long userId, String userCode, List<String> inputList) {
        // 对用户代码进行判断
        // 要执行 代码 首先要有用户的代码文件
        // 1.创建一个代码文件出来
        createUserCodeFile(userId, userCode);

        //  2. 创建docker容器出来 ,容器有java8
        initDockerSanBox();

        // 3  . 编译代码
        CompileResult compileResult = compileCodeByDocker();
        // 如果编译失败了
        if (!compileResult.isCompiled()) {

            // 回收资源
            // 清理掉 宿主机存储代码的 文件 和 容器
            deleteUserCodeFile(); // 删除文件
            deleteContainer();  // 删除容器
            // 返回错误信息
            return SandBoxExecuteResult.fail(CodeRunStatus.COMPILE_FAILED, compileResult.getExeMessage());
        }
        // 4. 执行代码
         return executeJavaCodeByDocker(inputList);
    }


    // 创建用户的代码文件
    private void createUserCodeFile(Long userId, String userCode) {

        String examCodeDir = System.getProperty("user.dir") + File.separator + JudgeConstants.EXAM_CODE_DIR;
        if (!FileUtil.exist(examCodeDir)) {
            // 创建存放用户代码的目录
            FileUtil.mkdir(examCodeDir);
        }
        // 设置文件日期
        String time = LocalDateTimeUtil.format(LocalDateTime.now(), DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
        // 拼接用户代码文件 格式
        // 区分 用户的代码文件  目录: 用户id + _  + 时间
        userCodeDir = examCodeDir + File.separator + userId + Constants.UNDERLINE_SEPARATOR + time;
        // 用户文件名字
        userCodeFileName = userCodeDir + File.separator + JudgeConstants.USER_CODE_JAVA_CLASS_NAME;
        FileUtil.writeString(userCode, userCodeFileName, Constants.UTF8);
    }

    // 创建一个 拥有java8的容器,来执行 代码
    private void initDockerSanBox() {
        // 创建docker
        DefaultDockerClientConfig clientConfig = DefaultDockerClientConfig.createDefaultConfigBuilder()
                .withDockerHost(dockerHost)
                .build();
        // 保存docker的  连接
        dockerClient = DockerClientBuilder
                .getInstance(clientConfig)
                .withDockerCmdExecFactory(new NettyDockerCmdExecFactory())
                .build();

        //拉取镜像: 需要一个java8的镜像
        pullJavaEnvImage();
        //创建容器  限制资源,权限
        HostConfig hostConfig = getHostConfig();
        // 创建容器命令
        CreateContainerCmd containerCmd = dockerClient
                // 使用哪个镜像
                .createContainerCmd(JudgeConstants.JAVA_ENV_IMAGE)
                // 容器的名字
                .withName(JudgeConstants.JAVA_CONTAINER_NAME);
        CreateContainerResponse createContainerResponse = containerCmd
                .withHostConfig(hostConfig)
                .withAttachStderr(true)   // 执行的结果无论是正确还是错误
                .withAttachStdout(true)   // 都英爱获取到
                .withTty(true)          // 配置一个伪终端,方便后续执行代码
                .exec();
        //记录容器id
        containerId = createContainerResponse.getId();
        //启动容器
        dockerClient.startContainerCmd(containerId).exec();
    }

    //限制资源 ,容器的权限
    private HostConfig getHostConfig() {
        HostConfig hostConfig = new HostConfig();
        //设置挂载目录，指定用户代码路径,
        // 把 要执行的代码 在宿主机 userCodeDir 目录上 , 将这个目录挂载到容器的 "/usr/share/java 目录上
        hostConfig.setBinds(new Bind(userCodeDir, new Volume(JudgeConstants.DOCKER_USER_CODE_DIR)));
        //限制docker容器使用资源
        hostConfig.withMemory(memoryLimit); // 内存限制
        hostConfig.withMemorySwap(memorySwapLimit); // 交换限制
        hostConfig.withCpuCount(cpuLimit);  // cpu限制
        hostConfig.withNetworkMode("none");  //禁用网络
        hostConfig.withReadonlyRootfs(true); //禁止在root目录写文件
        return hostConfig;
    }


    //拉取java执行环境镜像 需要控制只拉取一次
    private void pullJavaEnvImage() {
        ListImagesCmd listImagesCmd = dockerClient.listImagesCmd();
        List<Image> imageList = listImagesCmd.exec();
        // 遍历 docker 拥有的 镜像列表,如果有了java8的镜像就不需要再拉取镜像
        for (Image image : imageList) {
            String[] repoTags = image.getRepoTags();
            if (repoTags != null && repoTags.length > 0 && JudgeConstants.JAVA_ENV_IMAGE.equals(repoTags[0])) {
                return;
            }
        }
        PullImageCmd pullImageCmd = dockerClient.pullImageCmd(JudgeConstants.JAVA_ENV_IMAGE);
        try {
            //            用来监听 docker , 同时 .await 等待它拉取镜像完成
            pullImageCmd.exec(new PullImageResultCallback()).awaitCompletion();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }


    //编译: 使用docker编译
    private CompileResult compileCodeByDocker() {
        // 编译指令 , 入参 (编译这里不需要) , 容器id, 用哪个容器执行指令
        String cmdId = createExecCmd(JudgeConstants.DOCKER_JAVAC_CMD, null, containerId);

        // 使用 这个Callback 来监听 编译指令 执行的过程 有没有出现错误
        DockerStartResultCallback resultCallback = new DockerStartResultCallback();
        // 编译的结果
        CompileResult compileResult = new CompileResult();
        try {
            dockerClient.execStartCmd(cmdId)
                    .exec(resultCallback)

                    .awaitCompletion();
            if (CodeRunStatus.FAILED.equals(resultCallback.getCodeRunStatus())) {
                // 如果 执行的结果为 有错误 , 编译结果为 false ,同时加上醋五信息
                compileResult.setCompiled(false);
                compileResult.setExeMessage(resultCallback.getErrorMessage());
            } else {
                // 编译成功, 编译设置为 true
                compileResult.setCompiled(true);
            }
            return compileResult;
        } catch (InterruptedException e) {
            //此处可以直接抛出 已做统一异常处理  也可再做定制化处理
            throw new RuntimeException(e);
        }
    }


    // 执行java代码               需要输入的参数
    private SandBoxExecuteResult executeJavaCodeByDocker(List<String> inputList) {
        List<String> outList = new ArrayList<>(); //记录输出结果
        long maxMemory = 0L;  //最大占用内存
        long maxUseTime = 0L; //最大运行时间
        //执行用户代码
        for (String inputArgs : inputList) {
            // 构造执行指令
            String cmdId = createExecCmd(JudgeConstants.DOCKER_JAVA_EXEC_CMD, inputArgs, containerId);
            //执行代码

            StopWatch stopWatch = new StopWatch();        //执行代码后开始计时
            //执行情况监控
            StatsCmd statsCmd = dockerClient.statsCmd(containerId); //启动监控

            // 占用内存的 监听 , 监听容器使用的 最大内存是多少
            StatisticsCallback statisticsCallback = statsCmd.exec(new StatisticsCallback());

            // 执行代码之前, 开启计时器
            stopWatch.start();
            // 执行代码的结果 统计, 最后执行结束后的结果
            DockerStartResultCallback resultCallback = new DockerStartResultCallback();
            try {
                // 执行代码
                dockerClient.execStartCmd(cmdId)
                        .exec(resultCallback)
                        // await 需要传参 , 上面编译的时候不需要限制 超时时间
                        // 这里执行 代码需要有 超时时间
                        .awaitCompletion(timeLimit, TimeUnit.SECONDS);
                if (CodeRunStatus.FAILED.equals(resultCallback.getCodeRunStatus())) {
                    // 只要有一个用例没有完成通过 , 返回错误信息
                    return SandBoxExecuteResult.fail(CodeRunStatus.NOT_ALL_PASSED);
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            stopWatch.stop();  //结束时间统计
            statsCmd.close();  //结束docker容器执行 内存 统计

            long userTime = stopWatch.getLastTaskTimeMillis(); //执行耗时
            maxUseTime = Math.max(userTime, maxUseTime);       //记录最大的执行用例耗时
            Long memory = statisticsCallback.getMaxMemory();  //  最大内存消耗
            if (memory != null) {
                maxMemory = Math.max(maxMemory, statisticsCallback.getMaxMemory()); //记录最大的执行用例占用内存
            }

            outList.add(resultCallback.getMessage().trim());   //记录正确的输出结果
        }
        deleteContainer();//删除容器
        deleteUserCodeFile(); //清理文件

        return getSanBoxResult(inputList, outList, maxMemory, maxUseTime); //封装结果
    }


    // 封装正确的返回结果
    private SandBoxExecuteResult getSanBoxResult(List<String> inputList, List<String> outList,
                                                 long maxMemory, long maxUseTime) {
     // 最后判断一次结果
        if(inputList.size() != outList.size()){
            // 如果输入参数的个数和 输出个数不同, 那么就是 不正确的
            // 返回实际输出 例子
                return SandBoxExecuteResult.fail(CodeRunStatus.NOT_ALL_PASSED , outList , maxMemory , maxUseTime);
        }
        // 回答正确 返回
        return SandBoxExecuteResult.success(CodeRunStatus.SUCCEED , outList , maxMemory , maxUseTime );
    }


    // 创建指令 , 编译要执行的指令 ,运行要执行的指令, 都可以通过这个方法来构造 ;
    private String createExecCmd(String[] javaCmdArr, String inputArgs, String containerId) {
        if (!StrUtil.isEmpty(inputArgs)) {
            //当入参不为空时拼接入参
            // split(空格) , 通过空格分开各个参数 , 如果有多个参数 : a b 都是用 空格分开
            String[] inputArray = inputArgs.split(" "); //入参
            javaCmdArr = ArrayUtil.append(JudgeConstants.DOCKER_JAVA_EXEC_CMD, inputArray);
        }
        ExecCreateCmdResponse cmdResponse = dockerClient.execCreateCmd(containerId)
                .withCmd(javaCmdArr)
                .withAttachStderr(true)
                .withAttachStdin(true)
                .withAttachStdout(true)
                .exec();
        return cmdResponse.getId();
    }

    // 清理掉文件
    private void deleteUserCodeFile() {
        FileUtil.del(userCodeDir);
    }

    // 删除掉创建的docker容器
    private void deleteContainer() {
        // 执行完代码后删除容器
        // 停止容器
        dockerClient.stopContainerCmd(containerId).exec();
        // 删除容器
        dockerClient.removeContainerCmd(containerId).exec();
        // 断开和docker的连接
        try {
            dockerClient.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

}